/*
 * Copyright (c) 2013-2015 Charkey. All rights reserved.
 *
 * This software is the confidential and proprietary information of Charkey.
 * You shall not disclose such Confidential Information and shall use it only
 * in accordance with the terms of the agreements you entered into with Charkey.
 *
 * Charkey MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE SOFTWARE,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT.
 *
 * Charkey SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING,
 * MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
 */

package cn.simastudio.talos.core.service.base.impl;

import cn.simastudio.talos.common.utils.RandomStringGenerator;
import cn.simastudio.talos.core.exception.auth.UserPasswordNotMatchAuthException;
import cn.simastudio.talos.core.exception.auth.UserPasswordRetryLimitExceedAuthException;
import cn.simastudio.talos.core.model.base.User;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;

/**
 * 用于密码服务：加密用户密码、校验密码是否匹配
 */
@Service
public class PasswordService {

    private static final Logger LOGGER = LoggerFactory.getLogger(PasswordService.class);

    @Autowired
    private CacheManager cacheManager;

    private Cache passwordRetryCache;

    /**
     * 默认密码长度
     */
    private int defaultPasswordLength = 8;

    //Todo: maxRetryCount最大重试次数需要可配置

    /**
     * 最大可重试次数
     */
    private int maxRetryCount = 10;

    /**
     * 随机字符串生成器
     */
    private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();

    /**
     * 随机字符串生成
     */
    private RandomStringGenerator randomStringGenerator = new RandomStringGenerator(defaultPasswordLength);

    /**
     * 加密算法名
     */
    private String algorithmName = "md5";

    /**
     * 哈希迭代次数
     */
    private int hashIterations = 2;

    public void setMaxRetryCount(int maxRetryCount) {
        this.maxRetryCount = maxRetryCount;
    }

    @PostConstruct
    public void init() {
        passwordRetryCache = cacheManager.getCache("passwordRetryCache");
    }

    /**
     * 校验用户密码是否正确
     *
     * @param user     User对象
     * @param password 用户输入的密码
     */
    public void validate(User user, String password) {
        String username = user.getUsername();

        int retryCount = 0;

        Element cacheElement = passwordRetryCache.get(username);
        if (cacheElement != null) {
            retryCount = (Integer) cacheElement.getObjectValue();
            if (retryCount >= maxRetryCount) {
                if (LOGGER.isErrorEnabled()) {
                    LOGGER.error(username + " passwordError " + " password error, retry limit exceed! password: {}, max retry count {}", password, maxRetryCount);
                }
                throw new UserPasswordRetryLimitExceedAuthException(maxRetryCount);
            }
        }

        if (!matches(user, password)) {
            passwordRetryCache.put(new Element(username, ++retryCount));
            if (LOGGER.isErrorEnabled()) {
                LOGGER.error(username + "passwordError " + " password error! password: {}, retry count: {}", password, retryCount);
            }
            throw new UserPasswordNotMatchAuthException();
        } else {
            clearPasswordRetryCache(username);
        }
    }

    /**
     * 校验登录凭证是否正确
     *
     * @param credential     用于登录的凭证，如用户名、手机号
     * @param salt           加密盐
     * @param storedPassword 已存储的密码
     * @param newPassword    登录时输入的密码
     */
    public void validate(String credential, String salt, String storedPassword, String newPassword) {
        int retryCount = 0;
        Element cacheElement = passwordRetryCache.get(credential);
        if (cacheElement != null) {
            retryCount = (Integer) cacheElement.getObjectValue();
            if (retryCount >= maxRetryCount) {
                if (LOGGER.isErrorEnabled()) {
                    LOGGER.error(credential + " passwordError " + " password error, retry limit exceed! password: {}, max retry count {}",
                            newPassword, maxRetryCount);
                }
                throw new UserPasswordRetryLimitExceedAuthException(maxRetryCount);
            }
        }
        if (!matches(salt, storedPassword, newPassword)) {
            passwordRetryCache.put(new Element(credential, ++retryCount));
            if (LOGGER.isErrorEnabled()) {
                LOGGER.error(credential + "passwordError " + " password error! password: {}, retry count: {}", newPassword, retryCount);
            }
            throw new UserPasswordNotMatchAuthException();
        }
    }

    /**
     * 通过加密算法校验用户名密码
     *
     * @param user        登录用户
     * @param newPassword 登录时输入的密码
     * @return true：密码匹配；false：密码不匹配
     */
    private boolean matches(User user, String newPassword) {
        return matches(user.getSalt(), user.getPassword(), newPassword);
    }

    /**
     * 通过加密算法校验用户名密码
     *
     * @param salt           加密盐
     * @param storedPassword 用户表中已加密的密码
     * @param newPassword    用户登陆时候输入的密码
     * @return 密码是否匹配（匹配：true；不匹配：false）
     */
    private boolean matches(String salt, String storedPassword, String newPassword) {
        return storedPassword.equals(encryptPassword(newPassword, salt));
    }

    /**
     * 清除对应登录用户的密码重试缓存
     *
     * @param credential 用于登录的凭证，如用户名、手机号
     */
    public void clearPasswordRetryCache(String credential) {
        passwordRetryCache.remove(credential);
    }

    /**
     * 密码加密
     *
     * @param password 用于登录的密码
     * @param salt     用于加密的加密盐，调用getRandomSalt获得
     * @return 加密之后的密码
     */
    public String encryptPassword(String password, String salt) {
        return new SimpleHash(algorithmName, password,
                ByteSource.Util.bytes(salt), hashIterations).toHex();
    }

    public void setRandomNumberGenerator(RandomNumberGenerator randomNumberGenerator) {
        this.randomNumberGenerator = randomNumberGenerator;
    }

    public void setAlgorithmName(String algorithmName) {
        this.algorithmName = algorithmName;
    }

    public void setHashIterations(int hashIterations) {
        this.hashIterations = hashIterations;
    }

    /**
     * 为User对象提供密码加密服务<br/>
     * <ul>
     * <li>为User对象设置随机加密盐</li>
     * <li>利用用户名、明文密码、加密盐进行密码加密并设置给User</li>
     * </ul>
     *
     * @param user 待设置salt和加密密码的User
     */
    public void encryptPassword(User user) {
        user.setSalt(randomNumberGenerator.nextBytes().toHex());
        String newPassword = encryptPassword(user.getPassword(), user.getSalt());
        user.setPassword(newPassword);
    }

    /**
     * 获取随机字符串作为加密盐
     *
     * @return 加密盐
     */
    public String getRandomSalt() {
        return randomNumberGenerator.nextBytes().toHex();
    }

    /**
     * 获得随机字符串作为初始密码
     *
     * @return 随机密码
     */
    public String getRandomPassword() {
        return randomStringGenerator.random();
    }
}
